/* global google */ (function () { "use strict"; var cfg = typeof window.travelgoHotelGooglePlacesEmbed === "object" && window.travelgoHotelGooglePlacesEmbed ? window.travelgoHotelGooglePlacesEmbed : {}; var S = cfg.strings || {}; var pageSize = Math.max(6, Math.min(30, parseInt(String(cfg.pageSize || "12"), 10) || 12)); var maxPages = Math.max(1, Math.min(10, parseInt(String(cfg.maxPages || "6"), 10) || 6)); var resultsPageUrl = typeof cfg.resultsPageUrl === "string" ? cfg.resultsPageUrl : ""; var BOOKING_ENABLED = !!cfg.bookingEnabled; var BOOKING_AJAX_URL = typeof cfg.bookingAjaxUrl === "string" ? cfg.bookingAjaxUrl : ""; var BOOKING_NONCE = typeof cfg.bookingNonce === "string" ? cfg.bookingNonce : ""; function esc(s) { var d = document.createElement("div"); d.textContent = s == null ? "" : String(s); return d.innerHTML; } function localIsoDate(d) { if (!d || isNaN(d.getTime())) { return ""; } var y = d.getFullYear(); var m = d.getMonth() + 1; var day = d.getDate(); return y + "-" + pad2(m) + "-" + pad2(day); } function pad2(n) { return n < 10 ? "0" + n : String(n); } function hashSeed(str) { var h = 0; var i; str = String(str || ""); for (i = 0; i < str.length; i++) { h = (h << 5) - h + str.charCodeAt(i); h |= 0; } return Math.abs(h); } function getPriceInrAmount(place) { var r = place.rating; if (typeof r !== "number" || !isFinite(r)) { r = 3.2; } var seed = hashSeed(place.place_id || place.name || ""); var pl = place.price_level; var tier = 0; if (typeof pl === "number" && isFinite(pl) && pl >= 0 && pl <= 4) { tier = pl; } // Google Places does not return real nightly rates — this is indicative only. // Old formula floored most listings around ₹2k+; widen range and use price_level when present. var fromTier = tier * 340; var fromRating = Math.round(r * 220); var spread = seed % 2200; return 399 + fromTier + fromRating + spread; } window.travelgoHotelGooglePlacesEmbedInit = function () { var root = document.querySelector(".travelgo-gpe-embed[data-travelgo-gpe-root][data-tgpe-page=\"results\"]") || document.querySelector(".travelgo-gpe-embed[data-travelgo-gpe-root]"); if (!root || typeof google === "undefined" || !google.maps || !google.maps.places) { return; } var pageMode = root.getAttribute("data-tgpe-page") === "results" ? "results" : "form"; var autocompleteService = new google.maps.places.AutocompleteService(); var mapDiv = root.querySelector("#tgpe-map-hidden"); if (!mapDiv) { return; } var map = new google.maps.Map(mapDiv, { center: { lat: 20, lng: 0 }, zoom: 2 }); var placesService = new google.maps.places.PlacesService(map); var sessionToken = new google.maps.places.AutocompleteSessionToken(); var allHotels = []; var seenIds = {}; var lastFiltered = []; var currentDestLabel = ""; var currentPageCount = 0; var currentMinRating = 0; var currentSort = "default"; var currentResultsPage = 0; var filterNameNeedle = ""; var destInput = root.querySelector("#tgpe-dest-input"); var pacDrop = root.querySelector("#tgpe-pac-drop"); var checkin = root.querySelector("#tgpe-checkin"); var checkout = root.querySelector("#tgpe-checkout"); var searchBtn = root.querySelector("#tgpe-search-btn"); var resultsWrap = root.querySelector("#tgpe-results-wrap"); var filterMinRating = root.querySelector("#tgpe-filter-min-rating"); var filterNameInput = root.querySelector("#tgpe-filter-name"); var filterMaxPriceInput = root.querySelector("#tgpe-filter-max-price"); var filterPropertyType = root.querySelector("#tgpe-filter-property-type"); var currencySelect = root.querySelector("#tgpe-currency-select"); var drawerToggle = root.querySelector("#tgpe-filter-drawer-toggle"); var drawerBackdrop = root.querySelector("#tgpe-mm-drawer-backdrop"); var drawerClose = root.querySelector("#tgpe-mm-drawer-close"); var sidebarEl = root.querySelector("#tgpe-mm-sidebar"); if (!destInput || !placesService || !searchBtn) { return; } if (pageMode === "results" && !resultsWrap) { return; } var currencyList = Array.isArray(cfg.currencies) && cfg.currencies.length ? cfg.currencies : [ { code: "INR", label: "INR \u20B9" }, { code: "USD", label: "USD $" }, ]; var fxRaw = cfg.fxFromInr && typeof cfg.fxFromInr === "object" ? cfg.fxFromInr : {}; function normalizeCur(c) { var u = String(c || "") .toUpperCase() .replace(/[^A-Z]/g, "") .slice(0, 3); var i; for (i = 0; i < currencyList.length; i++) { var code = String(currencyList[i].code || "") .toUpperCase() .replace(/[^A-Z]/g, "") .slice(0, 3); if (code && code === u) return code; } return ""; } var displayCurrency = normalizeCur(cfg.defaultCurrency || cfg.currency || "INR") || "INR"; var searchDone = false; var stallTimer = null; function clearStallTimer() { if (stallTimer) { clearTimeout(stallTimer); stallTimer = null; } } function armStallTimer(ms) { clearStallTimer(); stallTimer = setTimeout(function () { if (!searchDone) { finalizeOnce(); } }, ms); } function resetBtn() { if (searchBtn) { searchBtn.disabled = false; searchBtn.textContent = searchBtn.getAttribute("data-tgpe-label") || "SEARCH"; } } function setDates() { var t = new Date(); var ci = new Date(t); var co; if (root.classList.contains("travelgo-gpe--akbar")) { ci.setDate(ci.getDate() + 1); co = new Date(ci); co.setDate(co.getDate() + 1); } else { ci.setDate(ci.getDate() + 7); co = new Date(t); co.setDate(co.getDate() + 10); } if (checkin) checkin.value = localIsoDate(ci); if (checkout) checkout.value = localIsoDate(co); } var isAkbarForm = root.classList.contains("travelgo-gpe--akbar"); var roomsCount = 1; var guestsCount = 1; var checkinDisplay = root.querySelector("#tgpe-checkin-display"); var checkoutDisplay = root.querySelector("#tgpe-checkout-display"); var checkinDow = root.querySelector("#tgpe-checkin-dow"); var checkoutDow = root.querySelector("#tgpe-checkout-dow"); var nightsNum = root.querySelector("#tgpe-nights-num"); var nightsLbl = root.querySelector("#tgpe-nights-lbl"); var paxSummary = root.querySelector("#tgpe-pax-summary"); var paxTrigger = root.querySelector("#tgpe-pax-trigger"); var paxPop = root.querySelector("#tgpe-pax-pop"); var paxBackdrop = root.querySelector("#tgpe-pax-backdrop"); var paxApply = root.querySelector("#tgpe-pax-apply"); var roomsRow = root.querySelector("#tgpe-rooms-row"); var guestsRow = root.querySelector("#tgpe-guests-row"); function formatAkbarDate(iso) { if (!iso) { return { main: "\u2014", dow: "" }; } var d = new Date(iso + "T12:00:00"); if (isNaN(d.getTime())) { return { main: "\u2014", dow: "" }; } var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; var days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; return { main: d.getDate() + " " + months[d.getMonth()] + "'" + String(d.getFullYear()).slice(2), dow: days[d.getDay()], }; } function nightsBetween(ci, co) { if (!ci || !co) { return 1; } var a = new Date(ci + "T12:00:00"); var b = new Date(co + "T12:00:00"); var diff = Math.round((b.getTime() - a.getTime()) / 86400000); return Math.max(1, diff); } function updateAkbarDateDisplays() { if (!isAkbarForm) { return; } var ciFmt = formatAkbarDate(checkin ? checkin.value : ""); var coFmt = formatAkbarDate(checkout ? checkout.value : ""); if (checkinDisplay) checkinDisplay.textContent = ciFmt.main; if (checkinDow) checkinDow.textContent = ciFmt.dow; if (checkoutDisplay) checkoutDisplay.textContent = coFmt.main; if (checkoutDow) checkoutDow.textContent = coFmt.dow; var nights = nightsBetween(checkin ? checkin.value : "", checkout ? checkout.value : ""); if (nightsNum) nightsNum.textContent = String(nights); if (nightsLbl) { nightsLbl.textContent = nights === 1 ? S.nightSingular || "NIGHT" : S.nightPlural || "NIGHTS"; } } function updatePaxSummary() { if (!paxSummary) { return; } var roomWord = roomsCount === 1 ? S.roomSingular || "Room" : S.roomPlural || "Rooms"; var guestWord = guestsCount === 1 ? S.guestSingular || "Guest" : S.guestPlural || "Guests"; paxSummary.textContent = roomsCount + " " + roomWord + " " + guestsCount + " " + guestWord; } function renderAkbarNumRow(container, value, min, max, onChange) { if (!container) { return; } container.innerHTML = '' + '' + esc(value) + "" + ''; container.querySelectorAll("[data-delta]").forEach(function (btn) { btn.addEventListener("click", function () { var delta = parseInt(btn.getAttribute("data-delta"), 10) || 0; var next = Math.min(max, Math.max(min, value + delta)); if (next !== value) { value = next; onChange(value); renderAkbarNumRow(container, value, min, max, onChange); } }); }); } function closePaxPop() { if (paxPop) paxPop.setAttribute("hidden", "hidden"); if (paxBackdrop) paxBackdrop.setAttribute("hidden", "hidden"); if (paxTrigger) paxTrigger.setAttribute("aria-expanded", "false"); } function openPaxPop() { if (!paxPop) { return; } var cal = root.querySelector("#tgpe-date-cal"); if (cal && !cal.hasAttribute("hidden") && window.travelgoHotelAkbarCalendarInit) { var calBackdrop = root.querySelector("#tgpe-date-cal-backdrop"); cal.setAttribute("hidden", "hidden"); if (calBackdrop) { calBackdrop.setAttribute("hidden", "hidden"); } } renderAkbarNumRow(roomsRow, roomsCount, 1, 6, function (v) { roomsCount = v; }); renderAkbarNumRow(guestsRow, guestsCount, 1, 12, function (v) { guestsCount = v; }); paxPop.removeAttribute("hidden"); if (paxBackdrop) paxBackdrop.removeAttribute("hidden"); if (paxTrigger) paxTrigger.setAttribute("aria-expanded", "true"); } function wireAkbarHotelForm() { if (!isAkbarForm) { return; } if (root.getAttribute("data-tgpe-akbar-wired") === "1") { return; } if (typeof window.travelgoHotelAkbarCalendarInit === "function") { window.travelgoHotelAkbarCalendarInit(root); } function onDateChange() { if (checkin && checkout && checkin.value) { var co = new Date(checkout.value + "T12:00:00"); var ci = new Date(checkin.value + "T12:00:00"); if (!checkout.value || co <= ci) { var fix = new Date(ci); fix.setDate(fix.getDate() + 1); checkout.value = localIsoDate(fix); } } updateAkbarDateDisplays(); } root.addEventListener("travelgo:hotel-akbar-dates-updated", onDateChange); if (paxTrigger) { paxTrigger.addEventListener("click", function () { if (paxPop && paxPop.hasAttribute("hidden")) { openPaxPop(); } else { closePaxPop(); } }); } if (paxBackdrop) { paxBackdrop.addEventListener("click", closePaxPop); } if (paxApply) { paxApply.addEventListener("click", function () { updatePaxSummary(); closePaxPop(); }); } updateAkbarDateDisplays(); updatePaxSummary(); root.setAttribute("data-tgpe-akbar-wired", "1"); } function readUrlIntoForm() { try { var params = new URLSearchParams(window.location.search); var d = params.get("tgpe_dest"); var ci = params.get("tg_checkin"); var co = params.get("tg_checkout"); var rooms = params.get("tg_rooms"); var guests = params.get("tg_guests"); if (d) destInput.value = d; if (ci && checkin) checkin.value = ci; if (co && checkout) checkout.value = co; if (rooms) roomsCount = Math.max(1, parseInt(String(rooms), 10) || 1); if (guests) guestsCount = Math.max(1, parseInt(String(guests), 10) || 1); var curQ = normalizeCur(params.get("tgpe_cur") || ""); if (curQ) { displayCurrency = curQ; } else { try { var ls = localStorage.getItem("travelgo_gpe_currency"); if (ls && normalizeCur(ls)) displayCurrency = normalizeCur(ls); } catch (eLs) {} } } catch (e0) {} } function fxRateForCode(code) { var c = normalizeCur(code) || "INR"; var v = parseFloat(String(fxRaw[c])); if (c === "INR") { return 1; } if (isFinite(v) && v > 0) { return v; } return c === "USD" ? 0.012 : 1; } function fxActive() { return fxRateForCode(displayCurrency); } function inrToDisplayAmount(inr) { return Math.round(Number(inr) * fxActive()); } function displayToInrAmount(amt) { var fx = fxActive(); if (!fx) { return Number(amt); } return Number(amt) / fx; } function formatDisplayFromInr(inr) { var num = inrToDisplayAmount(inr); var c = displayCurrency; if (typeof Intl !== "undefined" && Intl.NumberFormat) { var loc = c === "INR" ? "en-IN" : "en-US"; try { return new Intl.NumberFormat(loc, { style: "currency", currency: c, maximumFractionDigits: 0, }).format(num); } catch (eIntl) { /* ignore */ } } if (c === "USD") { return "$" + num.toLocaleString("en-US"); } return "\u20B9" + num.toLocaleString("en-IN"); } if (pageMode === "results") { readUrlIntoForm(); } else { if (!root.getAttribute("data-tgpe-cal-wired")) { setDates(); } if (isAkbarForm) { updateAkbarDateDisplays(); } try { var p0 = new URLSearchParams(window.location.search); var c0 = normalizeCur(p0.get("tgpe_cur") || ""); if (c0) { displayCurrency = c0; } else { var ls0 = localStorage.getItem("travelgo_gpe_currency"); if (ls0 && normalizeCur(ls0)) { displayCurrency = normalizeCur(ls0); } } } catch (eLs0) {} } function hideDrop() { if (pacDrop) pacDrop.classList.remove("show"); var hero = document.querySelector(".travelgo-hero--akbar"); if (hero) hero.classList.remove("is-hotel-pac-open"); } var indianCityIata = { kolkata: "CCU", mumbai: "BOM", delhi: "DEL", "new delhi": "DEL", bangalore: "BLR", bengaluru: "BLR", hyderabad: "HYD", chennai: "MAA", goa: "GOI", kochi: "COK", cochin: "COK", jaipur: "JAI", lucknow: "LKO", ahmedabad: "AMD", pune: "PNQ", srinagar: "SXR", guwahati: "GAU", varanasi: "VNS", patna: "PAT", indore: "IDR", nagpur: "NAG", kolar: "KQZ", }; function iataForPlace(main, sec) { var key = String(main || "").toLowerCase().trim(); if (indianCityIata[key]) { return indianCityIata[key]; } if (sec && /india/i.test(sec)) { return ""; } return ""; } function rankPrediction(p) { var main = (p.structured_formatting && p.structured_formatting.main_text) || ""; var sec = (p.structured_formatting && p.structured_formatting.secondary_text) || ""; var score = 0; var mainKey = main.toLowerCase().trim(); if (/india/i.test(sec)) { score += 120; } if (indianCityIata[mainKey]) { score += 80; } if (/,\s*india$/i.test(sec)) { score += 40; } return score; } function showDrop(preds) { if (!pacDrop) return; var sorted = preds.slice().sort(function (a, b) { return rankPrediction(b) - rankPrediction(a); }); pacDrop.innerHTML = sorted .map(function (p) { var main = p.structured_formatting && p.structured_formatting.main_text ? p.structured_formatting.main_text : ""; var sec = p.structured_formatting && p.structured_formatting.secondary_text ? p.structured_formatting.secondary_text : ""; var desc = p.description || ""; var code = iataForPlace(main, sec); return ( '" ); }) .join(""); pacDrop.classList.add("show"); var hero = document.querySelector(".travelgo-hero--akbar"); if (hero) hero.classList.add("is-hotel-pac-open"); } function onInput() { var val = destInput.value; if (!val || val.length < 2) { hideDrop(); return; } var req = { input: val, sessionToken: sessionToken, }; var countryBias = cfg.hotelAutocompleteCountry !== undefined && cfg.hotelAutocompleteCountry !== null ? String(cfg.hotelAutocompleteCountry).trim() : "in"; if (countryBias) { req.componentRestrictions = { country: countryBias }; } autocompleteService.getPlacePredictions(req, function (preds, status) { if (status !== google.maps.places.PlacesServiceStatus.OK || !preds || !preds.length) { hideDrop(); return; } showDrop(preds.slice(0, 8)); }); } destInput.addEventListener("input", onInput); destInput.addEventListener("blur", function () { setTimeout(hideDrop, 180); }); destInput.addEventListener("keydown", function (e) { if (e.key === "Enter") { e.preventDefault(); doSearch(); } }); if (pacDrop) { var lastPacPickAt = 0; function pickPacItem(e) { if (!e || !e.target || !e.target.closest) return; var btn = e.target.closest(".tgpe-pac-item"); if (!btn || !pacDrop.contains(btn)) return; var now = Date.now(); if (now - lastPacPickAt < 350) return; lastPacPickAt = now; if (e.cancelable) e.preventDefault(); var d = btn.getAttribute("data-tgpe-desc"); if (d) { try { destInput.value = decodeURIComponent(d); } catch (e2) { destInput.value = d; } hideDrop(); } } if (typeof window.PointerEvent !== "undefined") { pacDrop.addEventListener("pointerdown", pickPacItem, true); } else { pacDrop.addEventListener("mousedown", pickPacItem, true); pacDrop.addEventListener( "touchend", function (e) { pickPacItem(e); }, { capture: true, passive: false } ); } } function goToResultsPage() { var dest = destInput.value.trim(); if (!dest) { destInput.focus(); return; } if (!resultsPageUrl) { window.alert(S.configureResults || "Configure a hotel results page in the theme."); return; } var params = new URLSearchParams(); params.set("tgpe_dest", dest); if (checkin && checkin.value) params.set("tg_checkin", checkin.value); if (checkout && checkout.value) params.set("tg_checkout", checkout.value); params.set("tg_rooms", String(roomsCount)); params.set("tg_guests", String(guestsCount)); params.set("tgpe_cur", displayCurrency); var sep = resultsPageUrl.indexOf("?") >= 0 ? "&" : "?"; window.location.href = resultsPageUrl + sep + params.toString(); } function syncResultsUrl() { if (pageMode !== "results") return; try { var params = new URLSearchParams(window.location.search); params.set("tgpe_dest", destInput.value.trim()); if (checkin && checkin.value) params.set("tg_checkin", checkin.value); if (checkout && checkout.value) params.set("tg_checkout", checkout.value); params.set("tg_rooms", String(roomsCount)); params.set("tg_guests", String(guestsCount)); params.set("tgpe_cur", displayCurrency); var q = params.toString(); if (window.history && window.history.replaceState) { window.history.replaceState({}, "", window.location.pathname + (q ? "?" + q : "")); } } catch (e1) {} } function mergeResults(results) { if (!results) return; var i; for (i = 0; i < results.length; i++) { var p = results[i]; if (!p || !p.place_id) continue; if (seenIds[p.place_id]) continue; seenIds[p.place_id] = true; allHotels.push(p); } } function buildStaysQuery(dest) { return ( "hotels homestays guest houses lodges hostels serviced apartments budget stays in " + dest ); } /** Second-phase query so the list also pulls low-cost segments Places ranks separately. */ function buildBudgetStaysQuery(dest) { return ( "cheap budget affordable economy low cost backpacker hostels motels dormitory PG paying guest capsule lodge rooms in " + dest ); } function finalizeOnce() { if (searchDone) { return; } searchDone = true; clearStallTimer(); resetBtn(); if (pageMode === "results") { syncResultsUrl(); renderResultsChrome(); applyFiltersAndSort(); } } function doSearch() { if (pageMode === "form") { goToResultsPage(); return; } var dest = destInput.value.trim(); if (!dest) { destInput.focus(); return; } currentDestLabel = dest; allHotels = []; seenIds = {}; lastFiltered = []; currentPageCount = 0; currentResultsPage = 0; searchDone = false; clearStallTimer(); armStallTimer(32000); if (searchBtn) { searchBtn.disabled = true; searchBtn.textContent = S.searching || "Searching…"; } if (resultsWrap) { resultsWrap.style.display = "block"; resultsWrap.innerHTML = '
' + '
' + '
' + esc(S.searchingIn || "Searching stays in") + ' ' + esc(dest.split(",")[0]) + "
" + '
' + '
' + esc(S.fetchingPages || "Fetching listings…") + "
"; } var compositePhase = 0; var pagesThisPhase = 0; var phases = [ { query: buildStaysQuery(dest), pageCap: maxPages }, { query: buildBudgetStaysQuery(dest), pageCap: Math.max(2, Math.min(4, maxPages)), }, ]; function showNoResultsEmpty() { searchDone = true; clearStallTimer(); if (resultsWrap) { resultsWrap.innerHTML = '
' + '
' + esc(S.noneFound || "No stays found") + "
" + '

"' + esc(currentDestLabel) + "". " + esc(S.tryOther || "Try another search.") + "

"; } resetBtn(); } function goNextPhaseOrFinish() { if (compositePhase + 1 < phases.length) { compositePhase++; pagesThisPhase = 0; kickTextSearchPhase(); return; } finalizeOnce(); } function kickTextSearchPhase() { var ph = phases[compositePhase]; if (!ph) { finalizeOnce(); return; } try { placesService.textSearch({ query: ph.query }, onTextResults); } catch (eKick) { if (compositePhase + 1 < phases.length) { goNextPhaseOrFinish(); } else { if (allHotels.length > 0) { finalizeOnce(); } else { showNoResultsEmpty(); } } } } function onTextResults(results, status, pagination) { if (searchDone) { return; } var prog = resultsWrap ? resultsWrap.querySelector("#tgpe-prog") : null; var pageStatus = resultsWrap ? resultsWrap.querySelector("#tgpe-page-status") : null; var ph = phases[compositePhase]; var cap = ph ? ph.pageCap : maxPages; if (status === google.maps.places.PlacesServiceStatus.OK && results && results.length) { mergeResults(results); currentPageCount++; pagesThisPhase++; if (prog) prog.style.width = Math.min(92, 10 + currentPageCount * 12) + "%"; if (pageStatus) { var tail = pagination && pagination.hasNextPage && pagesThisPhase < cap ? "…" : compositePhase + 1 < phases.length ? " · " + (S.fetchingBudgetStays || "Adding budget stays…") : ""; pageStatus.textContent = (S.foundSoFar || "Found") + " " + allHotels.length + " " + (S.properties || "properties") + tail; } if (pagination && pagination.hasNextPage && pagesThisPhase < cap) { armStallTimer(16000); setTimeout(function () { if (searchDone) { return; } try { pagination.nextPage(); } catch (e3) { goNextPhaseOrFinish(); } }, 1600); } else { goNextPhaseOrFinish(); } } else { if (compositePhase + 1 < phases.length) { goNextPhaseOrFinish(); } else if (allHotels.length > 0) { finalizeOnce(); } else { showNoResultsEmpty(); } } } try { kickTextSearchPhase(); } catch (eTs) { if (!searchDone && compositePhase + 1 < phases.length) { goNextPhaseOrFinish(); } else if (!searchDone && allHotels.length > 0) { finalizeOnce(); } else if (!searchDone) { searchDone = true; clearStallTimer(); resetBtn(); if (resultsWrap) { resultsWrap.innerHTML = '
' + esc(S.noneFound || "No stays found") + "

" + esc(S.tryOther || "Try again or refresh the page.") + "

"; } } } } if (searchBtn) { searchBtn.addEventListener("click", doSearch); } function getPhotoUrl(place, maxW) { maxW = maxW || 500; if (place.photos && place.photos.length > 0) { return place.photos[0].getUrl({ maxWidth: maxW, maxHeight: 280 }); } return null; } function renderStars(r) { if (!r) return "\u2013"; var f = Math.floor(r); var h = r % 1 >= 0.4; var s = ""; var i; for (i = 0; i < f; i++) s += "\u2605"; if (h) s += "\u00bd"; return s; } function classifyProperty(h) { var name = String((h && h.name) || "").toLowerCase(); var types = Array.isArray(h.types) ? h.types : []; if ( /\b(homestay|guest\s*house|guesthouse|hostel|dormitory|dorm\b|pg\b|paying guest|bnb|bed\s*&?\s*breakfast)\b/i.test( name ) ) { return "homestay"; } var lodgingish = types.some(function (t) { return ( t === "lodging" || t === "resort_hotel" || t === "motel" || t === "campground" || t === "rv_park" ); }); if (lodgingish) { return "hotel"; } if ( /\b(hotel|resort|suites|palace|inn|tower|plaza|club|international)\b/i.test(name) ) { return "hotel"; } if ( /\b(lodge|hostel|motel|service apartment)\b/i.test(name) ) { return "homestay"; } return "other"; } function rowBadgesHtml(h) { var r = h.rating || 0; var cnt = h.user_ratings_total || 0; var amt = getPriceInrAmount(h); var seed = hashSeed(h.place_id || h.name || ""); var parts = []; if (r >= 4.2) { parts.push('' + esc(S.badgeTopRated || "Top rated") + ""); } if (cnt >= 45 && seed % 4 !== 0) { parts.push('' + esc(S.badgePopular || "Popular") + ""); } if (amt <= 1650 && r >= 3.5) { parts.push('' + esc(S.badgeValue || "Great value") + ""); } return parts.length ? '
' + parts.join("") + "
" : ""; } function mapsPlaceUrl(pid) { return "https://www.google.com/maps/place/?q=place_id:" + encodeURIComponent(pid); } function findHotelByPlaceId(pid) { if (!pid) return null; var j; for (j = 0; j < allHotels.length; j++) { if (allHotels[j] && allHotels[j].place_id === pid) return allHotels[j]; } return null; } function buildSearchBookingObject() { return { q: currentDestLabel, check_in_date: checkin ? checkin.value : "", check_out_date: checkout ? checkout.value : "", adults: 2, children: 0, currency: displayCurrency, }; } function buildHotelBookingObject(h) { var u = getPhotoUrl(h, 800); var pid = h.place_id || ""; return { name: h.name || "Stay", type: "Google Places", property_token: "tg_gplace_" + pid, google_place_id: pid, link: mapsPlaceUrl(pid), thumbnail: u || "", overall_rating: h.rating != null ? h.rating : "", reviews: h.user_ratings_total || 0, price: inrToDisplayAmount(getPriceInrAmount(h)), }; } function startBooking(h, btn) { if (!BOOKING_ENABLED || !BOOKING_AJAX_URL || !BOOKING_NONCE) { window.alert(S.bookingNotConfigured || "Hotel booking is not configured."); return; } var payload = { search: buildSearchBookingObject(), hotel: buildHotelBookingObject(h), }; btn.disabled = true; var fd = new FormData(); fd.append("action", "travelgo_save_hotel_booking"); fd.append("nonce", BOOKING_NONCE); fd.append("payload", JSON.stringify(payload)); fetch(BOOKING_AJAX_URL, { method: "POST", body: fd, credentials: "same-origin" }) .then(function (r) { return r.json(); }) .then(function (res) { btn.disabled = false; if (!res || !res.success || !res.data || !res.data.redirect) { window.alert((res && res.data && res.data.message) || S.bookingSaveError || "Could not start booking."); return; } window.location.href = res.data.redirect; }) .catch(function () { btn.disabled = false; window.alert(S.bookingSaveError || "Could not start booking."); }); } function hotelRow(h) { var photoUrl = getPhotoUrl(h); var imgHtml = photoUrl ? '' +
          esc(h.name) +
          '' : ''; var r = h.rating || 0; var cnt = h.user_ratings_total || 0; var amt = getPriceInrAmount(h); var price = formatDisplayFromInr(amt); var openHtml = h.opening_hours ? '' + (h.opening_hours.open_now ? S.openNow || "Open" : S.closed || "Closed") + "" : ""; var addr = (h.formatted_address || h.vicinity || "").split(",").slice(0, 3).join(","); var rid = (h.place_id || "").replace(/[^a-z0-9]/gi, ""); var reviewsHtml = (h.reviews || []) .slice(0, 1) .filter(function (rv) { return rv.text && rv.text.length > 10; }) .map(function (rv) { var t = rv.text.slice(0, 140) + (rv.text.length > 140 ? "\u2026" : ""); return '
' + esc(t) + "
"; }) .join(""); var bookBtn = BOOKING_ENABLED && h.place_id ? '" : ""; var mapLink = '' + esc(S.viewOnMap || "View on map") + ""; return ( '
' + '
' + imgHtml + "
" + '
' + '

' + esc(h.name) + "

" + rowBadgesHtml(h) + '
📍 ' + esc(addr) + " " + openHtml + "
" + (r ? '
' + renderStars(r) + '' + r.toFixed(1) + '(' + cnt.toLocaleString("en-IN") + ")
" : "") + '
' + (reviewsHtml || "") + "
" + '
' + '' + esc(S.from || "from") + "" + '
' + esc(price) + "
" + '' + esc(S.perNight || "per night") + '
' + bookBtn + mapLink + "
" ); } function renderResultsChrome() { if (!resultsWrap) return; var ci = checkin ? checkin.value : ""; var co = checkout ? checkout.value : ""; var nights = ci && co ? Math.max(1, Math.round((new Date(co) - new Date(ci)) / 86400000)) : 3; var city = currentDestLabel.split(",")[0]; resultsWrap.innerHTML = '
' + '
' + '

' + esc(S.hotelsIn || "Stays in") + " " + esc(city) + "

" + '

' + esc(String(allHotels.length)) + " " + esc(S.properties || "listings") + " \u00b7 " + esc(String(nights)) + " " + (nights > 1 ? esc(S.nights || "nights") : esc(S.night || "night")) + "

" + '
' + '
' + '' + esc(S.sort || "Sort") + ":" + '" + '" + '" + '" + '
" + '
' + '' + '
' + esc( S.showingFromGoogle && S.showingFromGoogle.indexOf("%s") >= 0 ? S.showingFromGoogle.replace(/%s/g, displayCurrency) : S.showingFromGoogle || "Listings from Google Places · indicative nightly prices (" + displayCurrency + ")" ) + "
"; wireSortFilter(); updateCurrencyDependentUi(); } function wireHotelImageFallbacks(container) { (container || resultsWrap).querySelectorAll("img.tgpe-hotel-img").forEach(function (img) { img.addEventListener( "error", function ph() { img.removeEventListener("error", ph); var d = document.createElement("div"); d.className = "tgpe-hotel-img-ph tgpe-hotel-row__img"; d.setAttribute("role", "img"); d.textContent = String.fromCodePoint(0x1f3e8); img.replaceWith(d); }, { once: true } ); }); } function wireCardClicks() { if (!resultsWrap) return; resultsWrap.querySelectorAll(".tgpe-hotel-row").forEach(function (card) { card.addEventListener("click", function (ev) { if (ev.target.closest("[data-tgpe-stop-row]")) return; var pid = card.getAttribute("data-tgpe-place-id"); if (!pid) return; var url = mapsPlaceUrl(pid); try { window.open(url, "_blank", "noopener,noreferrer"); } catch (e2) { window.location.href = url; } }); }); } function wireBookButtons() { if (!resultsWrap) return; resultsWrap.querySelectorAll(".tgpe-hotel-book").forEach(function (btn) { btn.addEventListener("click", function (ev) { ev.preventDefault(); ev.stopPropagation(); var pid = btn.getAttribute("data-tgpe-place-id"); var h = findHotelByPlaceId(pid); if (!h) return; startBooking(h, btn); }); }); } function wireSortFilter() { if (!resultsWrap) return; resultsWrap.querySelectorAll("[data-tgpe-sort]").forEach(function (btn) { btn.addEventListener("click", function () { currentSort = btn.getAttribute("data-tgpe-sort") || "default"; resultsWrap.querySelectorAll(".tgpe-sort-btn").forEach(function (b) { b.classList.remove("active"); }); btn.classList.add("active"); currentResultsPage = 0; applyFiltersAndSort(); }); }); } function wirePagerDelegation() { if (!resultsWrap || resultsWrap._tgpePagerBound) return; resultsWrap._tgpePagerBound = true; resultsWrap.addEventListener("click", function (ev) { var t = ev.target.closest("[data-tgpe-page-go]"); if (!t || !resultsWrap.contains(t)) return; ev.preventDefault(); var go = t.getAttribute("data-tgpe-page-go"); if (go === "prev") { currentResultsPage = Math.max(0, currentResultsPage - 1); } else if (go === "next") { var pages = Math.max(1, Math.ceil(lastFiltered.length / pageSize)); currentResultsPage = Math.min(pages - 1, currentResultsPage + 1); } else { var n = parseInt(go, 10); if (!isNaN(n)) currentResultsPage = n; } renderListPage(); }); } function renderListPage() { if (!resultsWrap) return; var list = resultsWrap.querySelector("#tgpe-hotel-list"); var pager = resultsWrap.querySelector("#tgpe-pager"); if (!list || !pager) return; var total = lastFiltered.length; var pages = Math.max(1, Math.ceil(total / pageSize) || 1); if (currentResultsPage >= pages) currentResultsPage = pages - 1; if (currentResultsPage < 0) currentResultsPage = 0; var start = currentResultsPage * pageSize; var slice = lastFiltered.slice(start, start + pageSize); list.innerHTML = slice.length ? slice.map(hotelRow).join("") : '
' + esc(S.noMatchFilter || "No match.") + "
"; var end = start + slice.length; var ps = S.pagerShowing || "Showing"; var po = S.pagerOf || "of"; var pprev = S.pagerPrev || "Previous"; var pnext = S.pagerNext || "Next"; var ppage = S.pagerPage || "Page"; pager.innerHTML = '
' + '" + '' + esc(ps) + " " + (total ? start + 1 : 0) + "\u2013" + end + " " + esc(po) + " " + total + "" + '' + esc(ppage) + " " + (currentResultsPage + 1) + " / " + pages + "" + '
"; wireCardClicks(); wireBookButtons(); wireHotelImageFallbacks(list); wirePagerDelegation(); fetchReviewsBatch(slice.slice(0, 28)); } function applyFiltersAndSort() { if (pageMode !== "results" || !resultsWrap) return; if (filterMinRating) { currentMinRating = parseFloat(filterMinRating.value || "0") || 0; } var n = filterNameNeedle.trim().toLowerCase(); var maxP = filterMaxPriceInput ? parseFloat(String(filterMaxPriceInput.value || "")) : NaN; var propType = filterPropertyType ? String(filterPropertyType.value || "all") : "all"; var filtered = allHotels.filter(function (h) { if ((h.rating || 0) < currentMinRating) return false; if (n && (h.name || "").toLowerCase().indexOf(n) === -1) return false; if (!isNaN(maxP) && maxP > 0 && getPriceInrAmount(h) > displayToInrAmount(maxP)) return false; var cls = classifyProperty(h); if (propType === "hotel" && cls === "homestay") return false; if (propType === "homestay" && cls === "hotel") return false; return true; }); if (currentSort === "rating") { filtered.sort(function (a, b) { return (b.rating || 0) - (a.rating || 0); }); } else if (currentSort === "reviews") { filtered.sort(function (a, b) { return (b.user_ratings_total || 0) - (a.user_ratings_total || 0); }); } else if (currentSort === "price_asc") { filtered.sort(function (a, b) { return getPriceInrAmount(a) - getPriceInrAmount(b); }); } else if (currentSort === "price_desc") { filtered.sort(function (a, b) { return getPriceInrAmount(b) - getPriceInrAmount(a); }); } lastFiltered = filtered; var badge = resultsWrap.querySelector("#tgpe-count-badge"); if (badge) { var ci = checkin ? checkin.value : ""; var co = checkout ? checkout.value : ""; var nights = ci && co ? Math.max(1, Math.round((new Date(co) - new Date(ci)) / 86400000)) : 3; badge.textContent = filtered.length + " " + (S.propertiesNights || "stays") + " \u00b7 " + nights + " " + (nights > 1 ? S.nights || "nights" : S.night || "night"); } renderListPage(); } function fetchReviewsBatch(hotels) { hotels.forEach(function (hotel, i) { if (!hotel || !hotel.place_id || hotel._detailsFetched) return; hotel._detailsFetched = true; setTimeout(function () { placesService.getDetails( { placeId: hotel.place_id, fields: ["reviews", "opening_hours"] }, function (detail, status) { if (status === google.maps.places.PlacesServiceStatus.OK && detail) { var idx = allHotels.findIndex(function (h) { return h.place_id === hotel.place_id; }); if (idx > -1) { allHotels[idx].reviews = detail.reviews || []; allHotels[idx].opening_hours = detail.opening_hours; } if (detail.reviews && detail.reviews.length) { var rid = hotel.place_id.replace(/[^a-z0-9]/gi, ""); var el = resultsWrap ? resultsWrap.querySelector("#tgpe-rv-" + rid) : null; if (el) { var html = detail.reviews .slice(0, 1) .filter(function (r) { return r.text && r.text.length > 10; }) .map(function (r) { var t = r.text.slice(0, 140) + (r.text.length > 140 ? "\u2026" : ""); return '
' + esc(t) + "
"; }) .join(""); if (html) el.innerHTML = html; } } } } ); }, i * 100); }); } function resetHotelFiltersDom() { filterNameNeedle = ""; if (filterNameInput) filterNameInput.value = ""; if (filterMinRating) filterMinRating.value = "0"; if (filterMaxPriceInput) filterMaxPriceInput.value = ""; if (filterPropertyType) filterPropertyType.value = "all"; currentMinRating = 0; currentSort = "default"; currentResultsPage = 0; if (resultsWrap) { resultsWrap.querySelectorAll(".tgpe-sort-btn").forEach(function (b, idx) { b.classList.toggle("active", idx === 0); }); } } if (sidebarEl) { sidebarEl.addEventListener("input", function (ev) { var tid = ev.target && ev.target.id; if (tid === "tgpe-filter-name") { clearTimeout(sidebarEl._tgpeNameDeb); sidebarEl._tgpeNameDeb = setTimeout(function () { filterNameNeedle = filterNameInput ? filterNameInput.value || "" : ""; currentResultsPage = 0; applyFiltersAndSort(); }, 220); return; } if (tid === "tgpe-filter-max-price") { clearTimeout(sidebarEl._tgpePriceDeb); sidebarEl._tgpePriceDeb = setTimeout(function () { currentResultsPage = 0; applyFiltersAndSort(); }, 280); } }); sidebarEl.addEventListener("change", function (ev) { var tid = ev.target && ev.target.id; if ( tid === "tgpe-filter-min-rating" || tid === "tgpe-filter-property-type" || tid === "tgpe-filter-max-price" ) { currentResultsPage = 0; applyFiltersAndSort(); } }); sidebarEl.addEventListener("click", function (ev) { var btn = ev.target && ev.target.closest("#tgpe-filter-reset"); if (!btn || !sidebarEl.contains(btn)) return; resetHotelFiltersDom(); applyFiltersAndSort(); closeFilterDrawer(); }); } function setFilterDrawer(open) { if (!sidebarEl) return; sidebarEl.classList.toggle("is-open", !!open); if (drawerBackdrop) { if (open) drawerBackdrop.removeAttribute("hidden"); else drawerBackdrop.setAttribute("hidden", ""); } if (drawerToggle) drawerToggle.setAttribute("aria-expanded", open ? "true" : "false"); if (drawerClose) drawerClose.hidden = !open; if (open) document.body.classList.add("tgpe-mm-drawer-open"); else document.body.classList.remove("tgpe-mm-drawer-open"); } function closeFilterDrawer() { setFilterDrawer(false); } function wireFilterDrawer() { if (!drawerToggle || !sidebarEl) return; drawerToggle.addEventListener("click", function () { var open = !sidebarEl.classList.contains("is-open"); setFilterDrawer(open); }); if (drawerBackdrop) { drawerBackdrop.addEventListener("click", closeFilterDrawer); } if (drawerClose) { drawerClose.addEventListener("click", closeFilterDrawer); } } function persistCurrencySelection() { try { localStorage.setItem("travelgo_gpe_currency", displayCurrency); } catch (e1) {} } function populateCurrencySelect(el) { if (!el) return; el.innerHTML = currencyList .map(function (x) { var code = normalizeCur(x.code); var lab = x.label != null ? String(x.label) : code; return '"; }) .join(""); el.value = displayCurrency; } function updateCurrencyDependentUi() { var maxLab = root.querySelector('label[for="tgpe-filter-max-price"]'); if (maxLab) { maxLab.textContent = (S.maxPricePerNight || "Max price / night") + " (" + displayCurrency + ")"; } var hint = root.querySelector(".tgpe-mm-sidebar .tgpe-mm-hint"); if (hint) { hint.textContent = S.priceIndicativeHint && S.priceIndicativeHint.indexOf("%s") >= 0 ? S.priceIndicativeHint.replace(/%s/g, displayCurrency) : S.priceIndicativeHint || "Indicative nightly prices in " + displayCurrency + ". Confirm on the booking page."; } var ribSym = root.querySelector("#tgpe-form-currency-symbol"); var ribTxt = root.querySelector("#tgpe-form-currency-copy"); if (ribSym) { ribSym.textContent = displayCurrency === "USD" ? "$" : "\u20B9"; } if (ribTxt) { ribTxt.textContent = displayCurrency === "USD" ? S.formRibbonUsd || "Indicative nightly prices in US dollars (estimate). Final price on booking." : S.formRibbonInr || "Indicative nightly prices in Indian rupees (estimate). Final price on booking."; } if (filterMaxPriceInput) { filterMaxPriceInput.setAttribute("step", displayCurrency === "USD" ? "1" : "50"); } var pageInfo = resultsWrap ? resultsWrap.querySelector(".tgpe-page-info") : null; if (pageInfo) { pageInfo.textContent = S.showingFromGoogle && S.showingFromGoogle.indexOf("%s") >= 0 ? S.showingFromGoogle.replace(/%s/g, displayCurrency) : S.showingFromGoogle || "Listings from Google Places · indicative nightly prices (" + displayCurrency + ")"; } } populateCurrencySelect(currencySelect); updateCurrencyDependentUi(); if (pageMode === "form" && isAkbarForm) { wireAkbarHotelForm(); } if (pageMode === "results" && isAkbarForm) { updateAkbarDateDisplays(); updatePaxSummary(); } if (currencySelect) { currencySelect.addEventListener("change", function () { var v = normalizeCur(currencySelect.value); if (!v) return; displayCurrency = v; persistCurrencySelection(); currencySelect.value = displayCurrency; updateCurrencyDependentUi(); if (pageMode === "results" && resultsWrap && allHotels.length) { syncResultsUrl(); applyFiltersAndSort(); } }); } if (pageMode === "results") { wirePagerDelegation(); wireFilterDrawer(); var autoDest = destInput.value.trim(); if (autoDest) { currentDestLabel = autoDest; // Maps invokes this callback before the map/Places stack is always ready; an immediate // textSearch() can throw or no-op until the first idle tick. Refresh often "fixes" it by // changing load timing — wait for idle (with a fallback) so the first load is reliable. var autoSearchKicked = false; function kickAutoSearchOnce() { if (autoSearchKicked) return; autoSearchKicked = true; doSearch(); } window.setTimeout(function () { kickAutoSearchOnce(); }, 1400); try { if ( google.maps && google.maps.event && typeof google.maps.event.addListenerOnce === "function" ) { google.maps.event.addListenerOnce(map, "idle", function () { window.setTimeout(kickAutoSearchOnce, 80); }); } else { window.setTimeout(kickAutoSearchOnce, 200); } } catch (eKick) { window.setTimeout(kickAutoSearchOnce, 200); } } } }; })();